import numpy as np
from sklearn.decomposition import PCA


class Recommend(object):
    @staticmethod
    def calc_svd(data, eps=1e-4):
        """
        svd计算，np.linalg.svd与手工计算

        :param eps: 奇异值<该值时，置为0
        :return:
        """
        m, n = data.shape
        # np.linalg.svd
        U, sigmal, VT = np.linalg.svd(data)
        sigmal = [0 if num < eps else num for num in sigmal]
        print('numpy U: {}'.format(U))
        print('numpy sigmal: {}'.format(sigmal))
        print('numpy VT: {}'.format(VT))

        sigmal_arr = []
        for i, num in enumerate(sigmal):
            if i >= n:
                mid_arr = [0] * n
            else:
                mid_arr = [0] * i + [num]
            while True:
                if len(mid_arr) < n:
                    mid_arr.append(0)
                else:
                    break
            sigmal_arr.append(mid_arr)

        while True:
            if len(sigmal_arr) < m:
                sigmal_arr.append([0] * n)
            else:
                break

        # print('sigmal_arr: {}'.format(sigmal_arr))
        n_mat = U * np.mat(sigmal_arr) * VT
        print('numpy计算的原始矩阵: {}'.format(n_mat))
        print('*' * 60)

        # calc
        """
        A = U * sigmal * VT
        sigmal为对角矩阵，值从大到小，为奇异值
        """
        # U
        A_AT = data * (data.T)
        A_AT_vals, A_AT_vectors = np.linalg.eig(A_AT)
        print('calc 特征向量/U: {}'.format(A_AT_vectors))

        # V
        AT_A = data.T * data
        AT_A_vals, AT_A_vectors = np.linalg.eig(AT_A)
        print('calc V: {}'.format(AT_A_vectors))

        # sigmal
        print('A * A.T特征值: {}'.format(A_AT_vals))
        sigmal = [0 if num < eps else num for num in np.sqrt(A_AT_vals)]
        print('sigmal: {}'.format(sigmal))

        sigmal_arr = []
        for i, num in enumerate(np.sqrt(A_AT_vals)):
            if i >= n:
                mid_arr = [0] * n
            else:
                mid_arr = [0] * i + [num]
            while True:
                if len(mid_arr) < n:
                    mid_arr.append(0)
                else:
                    break
            sigmal_arr.append(mid_arr)

        while True:
            if len(sigmal_arr) < m:
                sigmal_arr.append([0] * n)
            else:
                break

        f_mat = A_AT_vectors * np.mat(sigmal_arr) * (AT_A_vectors.T)
        print('手工计算的原始矩阵: {}'.format(f_mat))

    @staticmethod
    def calc_similar(vec_a, vec_b):
        """
        计算相似度
        :param vec_a:
        :param vec_b:
        :return:
        """
        def eclud(vec_a, vec_b):
            """
            欧氏距离
            :param vec_a:
            :param vec_b:
            :return:
            """
            # np.linalg.norm  求矩阵范数
            # dis = np.sqrt(np.power(vec_a - vec_b, 2).sum())
            dis = np.linalg.norm(vec_a - vec_b)
            return 1 / (1 + dis)

        def pearson(vec_a, vec_b):
            """
            皮尔逊相关系数
            约束条件：
            1、两个向量/变量有线性关系
            2、连续变量
            3、变量符合正态分布，且二元分布也符合正态分布
            4、两个变量独立

            上述条件任意不满足，则用spearman相关系数
            定序数据(优良差等)：反映观测对象等级、顺序关系的数据，用spearman，而非pearson
            :param vec_a:
            :param vec_b:
            :return:
            """
            if not isinstance(vec_a, np.matrix):
                vec_a = np.mat(vec_a)
            if not isinstance(vec_b, np.matrix):
                vec_b = np.mat(vec_b)

            if len(np.shape(vec_a)) > 1:
                vec_a = vec_a.T.A[0]
                vec_b = vec_b.T.A[0]

            if len(vec_a) == 1:
                return 0.5 + 0.5 * np.corrcoef([vec_a[0], vec_b[0]])

            return 0.5 + 0.5 * np.corrcoef(vec_a, vec_b)[0][1]

        def cosine(vec_a, vec_b):
            """
            余弦相似度

            :param vec_a:
            :param vec_b:
            :return:
            """
            if not isinstance(vec_a, np.matrix):
                vec_a = np.mat(vec_a)
            if not isinstance(vec_b, np.matrix):
                vec_b = np.mat(vec_b)

            if len(vec_a) > 1:
                a_dot_b = vec_a.T * vec_b

            else:
                a_dot_b = vec_a * vec_b.T
            a_dot_b = a_dot_b.A[0][0]

            # a_and_b = np.sqrt(np.power(vec_a, 2).sum()) * np.sqrt(np.power(vec_b, 2).sum())
            a_and_b = np.linalg.norm(vec_a) * np.linalg.norm(vec_b)

            return 0.5 + 0.5 * a_dot_b / a_and_b

        # 欧式
        eclud_similar = eclud(vec_a, vec_b)

        # pearson
        pearson_similar = pearson(vec_a, vec_b)

        # 余弦相似度
        cosine_similar = cosine(vec_a, vec_b)

        return cosine_similar

    def svd_es(self, data, user, item, x_transform_items):
        """
        基于SVD的评分估计
        :param data:
        :param user:
        :param similar_fun:
        :param item:
        :param x_transform_items: svd分解后的转换矩阵
        :return:
        """
        m, n = np.shape(data)
        similar_total = rate_similar_total = 0.0

        for i in range(n):
            user_rate_value = data[user, i]
            if user_rate_value == 0 or i == item:
                continue

            # 用转换后的两个物品的全部评分进行相似度计算
            similarity = self.calc_similar(x_transform_items[item, :].T, x_transform_items[i, :].T)
            print('the {} and {} similarity is: {}'.format(item, i, similarity))
            if np.isnan(similarity):
                continue
            similar_total += similarity
            rate_similar_total += similarity * user_rate_value

        if similar_total == 0:
            return 0
        return rate_similar_total / similar_total

    def stand_est(self, data, user, item, x_transform_items=None):
        """
        对物品idx为item的物品进行 加权评分

        评分值 = sum(评分值 * 相似度) / sum(相似度)

        :param data: 数据集
        :param user: 目标/推荐用户
        :param similar_fun: 相似值计算函数
        :param item: 目标评价物品的idx
        :return:
        """
        m, n = np.shape(data)
        similar_total = rate_similar_total = 0.0
        for i in range(n):
            user_rate_value = data[user, i]
            if user_rate_value == 0:
                continue

            # 寻找用户都评级的两个物品，并计算它们的相似度
            over_lap = np.nonzero(np.logical_and(data[:, item].A > 0, data[:, i].A > 0))[0]
            if len(over_lap) == 0:
                similarity = 0
            else:
                similarity = self.calc_similar(data[over_lap, item], data[over_lap, i])

            if np.isnan(similarity):
                continue
            similar_total += similarity
            rate_similar_total += similarity * user_rate_value

        if similar_total == 0:
            return 0
        return rate_similar_total / similar_total

    @staticmethod
    def recommend(data, user, N=3, est_method=stand_est, x_transform_items=None):
        """
        推荐评分靠前的前N个商品

        :param data: 数据集
        :param user: 目标/推荐用户
        :param N: 评分高的前n个
        :param similar_fun: 相似度的计算函数
        :param est_method: 根据相似度进行评分的函数
        :param x_transform_items: svd分解后的转换矩阵
        :return:
        """
        # 该用户没有参与评分的数据
        un_rate_item_idx = np.nonzero(data[user, :].A == 0)[1]
        if len(un_rate_item_idx) == 0:
            return '您对所有的物品均已评分!'
        item_scores = []
        for item_idx in un_rate_item_idx:
            estimated_score = est_method(data, user, item_idx, x_transform_items=x_transform_items)
            item_scores.append((item_idx, estimated_score))

        return sorted(item_scores, key=lambda d: d[1], reverse=True)[: N]

    @staticmethod
    def img_compress(data, thresh=0.8, num_sv=3):
        """
        图像压缩
        :param data:
        :param thresh:
        :return:
        """
        # 像素转换
        trans_data = np.array(data)
        trans_info = []
        for info in trans_data:
            info = [0 if x < thresh else 1 for x in info]
            trans_info.append(info)
        print(trans_info[0])

        U, sigmal, VT = np.linalg.svd(data)
        sig_recon = np.mat(np.zeros((num_sv, num_sv)))
        for k in range(num_sv):
            sig_recon[k, k] = sigmal[k]

        # 转换后的矩阵
        recon_mat = U[:, :num_sv] * sig_recon * VT[: num_sv, :]
        return recon_mat, thresh

    def run_simple(self):
        # data = [[1, 1], [7, 7]]
        # data = [[1, 1], [1, 1], [0, 0]]
        data = load_data()
        # 分解奇异值
        # calc_svd(np.mat(data))

        x = data[:, 0].T.A[0]
        y = data[:, 4].T.A[0]

        # 计算相似度
        self.calc_similar(vec_a=x, vec_b=y)

    def run_simple_recommand_stand(self):
        """
        简单推荐
        """
        data = load_data()
        data[4, 0] = data[4, 1] = data[5, 0] = data[6, 0] = 4
        data[0, 3] = 2
        print(data)

        recommand_info = self.recommend(data, user=6)
        print('recommand_info: {}'.format(recommand_info))

    def run_simple_recommand_svd(self):
        data = [[2, 0, 0, 4, 4, 0, 0, 0, 0, 0, 0],
                [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 5],
                [0, 0, 0, 0, 0, 0, 0, 1, 0, 4, 0],
                [3, 3, 4, 0, 3, 0, 0, 2, 2, 0, 0],
                [5, 5, 5, 0, 0, 0, 0, 0, 0, 0, 0],
                [0, 0, 0, 0, 0, 0, 5, 0, 0, 5, 0],
                [4, 0, 4, 0, 0, 0, 0, 0, 0, 0, 5],
                [0, 0, 0, 0, 0, 4, 0, 0, 0, 0, 4],
                [0, 0, 0, 0, 0, 0, 5, 0, 0, 5, 0],
                [0, 0, 0, 3, 0, 0, 0, 0, 4, 5, 0],
                [1, 1, 2, 1, 1, 2, 1, 0, 4, 5, 0]]
        data = np.mat(data)

        U, sigmal, VT = np.linalg.svd(data)

        """
        保留奇异值策略
        1、保留矩阵90%的能量信息: 奇异值平方和的累计值占总数的90%
        2、当矩阵有上万的奇异值时，保留前面的2000-3000个
        """
        # 计算最维度值
        sigmal_sum = np.power(sigmal, 2).cumsum() / np.power(sigmal, 2).sum()
        feature_num = len(sigmal_sum[sigmal_sum < 0.9])
        print('feature_num: {}'.format(feature_num))
        sigmal_mat = np.mat(np.eye(feature_num) * sigmal[:feature_num])

        # 构建转换后的物品矩阵
        x_transform_items = data.T * U[:, :feature_num] * sigmal_mat.I
        recommand_info = self.recommend(data, user=1, est_method=self.svd_es, x_transform_items=x_transform_items)
        print('recommand_info: {}'.format(recommand_info))

    def run_simple_recommand_pca_svd(self):
        data = [[2, 0, 0, 4, 4, 0, 0, 0, 0, 0, 0],
                [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 5],
                [0, 0, 0, 0, 0, 0, 0, 1, 0, 4, 0],
                [3, 3, 4, 0, 3, 0, 0, 2, 2, 0, 0],
                [5, 5, 5, 0, 0, 0, 0, 0, 0, 0, 0],
                [0, 0, 0, 0, 0, 0, 5, 0, 0, 5, 0],
                [4, 0, 4, 0, 0, 0, 0, 0, 0, 0, 5],
                [0, 0, 0, 0, 0, 4, 0, 0, 0, 0, 4],
                [0, 0, 0, 0, 0, 0, 5, 0, 0, 5, 0],
                [0, 0, 0, 3, 0, 0, 0, 0, 4, 5, 0],
                [1, 1, 2, 1, 1, 2, 1, 0, 4, 5, 0]]
        sk_pca = PCA(n_components=4)
        # svd_solver='auto'  ('auto', 'full', 'arpack', 'randomized')
        pca_data = sk_pca.fit_transform(data)

        recommand_info = self.recommend(np.mat(data), user=1, est_method=self.svd_es, x_transform_items=np.mat(pca_data))
        print('recommand_info: {}'.format(recommand_info))

    def run_img_compress(self, data):
        self.img_compress(data)


def load_data():
    return np.mat([[1, 1, 1, 0, 0],
                   [2, 2, 2, 0, 0],
                    [1, 1, 1, 0, 0],
                    [5, 5, 5, 0, 0],
                    [1, 1, 0, 2, 2],
                    [0, 0, 0, 3, 3],
                    [0, 0, 0, 1, 1]])


def load_img():
    file = './data/B/chp14_0_5.txt'
    with open(file, 'r') as f:
        data = f.readlines()

    data_arr = [list(info[: -1]) for info in data]
    data_arr = [list(map(int, info)) for info in data_arr]
    return np.mat(data_arr)


if __name__ == '__main__':
    """
    推荐引擎评价
    将已知的评分值去掉，对其进行预测，计算真实值与预测值之间的差异
    """
    recommend = Recommend()
    recommend.run_simple_recommand_svd()
    recommend.run_simple_recommand_pca_svd()
